﻿// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq;
using SnapObjects.Commons;

namespace SnapObjects.Data
{
    internal abstract class ModelEntryLazyParserBase<TModel>
    {
        protected readonly INodeReader _nodeReader;
        protected readonly RowFormat _rowFormat;
        protected readonly DataFormatSetting _setting;
        protected readonly IDictionary<string, MetaColumn> _metaColumns;
        protected readonly IModelMeta _modelMeta;
        protected int _sccessed = 0;
        protected readonly HashSet<string> _importeds;

        private readonly IPropertyMeta[] _propertyMetas;
        private readonly IDictionary<string, IPropertyMeta> _modelProperties;

        public ModelEntryLazyParserBase(
            INodeReader nodeReader,
            IModelMeta modelMeta,
            DataFormatSetting setting,
            IDictionary<string, MetaColumn> metaColumns,
            RowFormat rowFormat = RowFormat.Normal)
        {
            _nodeReader = nodeReader;
            _setting = setting;
            _rowFormat = rowFormat;
            _metaColumns = metaColumns;
            _modelMeta = modelMeta;
            _importeds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            if (modelMeta != null)
            {
                _propertyMetas = modelMeta.Properties.ToArray();

                _modelProperties = modelMeta.Properties.ToDictionary(
                    m => m.Name, m => m, StringComparer.OrdinalIgnoreCase);
            }
        }

        public bool IsEnd => _nodeReader.IsEnd;

        public bool Read()
        {
            return _nodeReader.Read();
        }

        protected void StanderdRowRead(ModelEntry<TModel> entry, INodeReader nodeReader, bool readOnly)
        {
            for (var index = 0; nodeReader.Read(); index++)
            {
                var currentNode = nodeReader.ReadAsNode();

                var (current, state, original) = this.GetNodeData(currentNode, index);

                if (!this.FilterColumn(index, currentNode.Name))
                {
                    continue;
                }

                if (!readOnly)
                {
                    this.SetPropertyEntry(entry, index, currentNode.Name, current, state, original, false);
                }
            }
        }

        protected void MoveNext(INodeReader node, params string[] expected)
        {
            if (!node.Read() || !expected.Contains(node.CurrentNodeName.ToLower()))
            {
                throw new FormatException(Properties.Resource.IMPORT_INCORRECTDATAFORMAT_EXCEPTION);
            }
        }

        protected INodeReader MoveNextValue(INodeReader node, params string[] expected)
        {
            this.MoveNext(node, expected);

            if (node.CurrentNodeType != DataNodeType.Value)
            {
                node = node.ReadAsNode();

                if (!node.Read())
                {
                    throw new FormatException(Properties.Resource.IMPORT_INCORRECTDATAFORMAT_EXCEPTION);
                }
            }

            return node;
        }

        protected void SetPropertyEntry(
            ModelEntry<TModel> entry,
            int nodeIndex,
            string name,
            object currentValue,
            PropertyState? propertyState = null,
            object originalValue = null,
            bool isSimple = true)
        {
            if (_modelMeta != null)
            {
                var propertymeta = this.GetPropertyMeta(nodeIndex, name);

                if (propertymeta == null || _importeds.Contains(propertymeta.Name))
                {
                    return;
                }

                name = propertymeta.Name;

                var isNew = entry.ModelState == ModelState.New || entry.ModelState == ModelState.NewModified;

                currentValue = this.GetValue(name, currentValue, propertymeta.DataType, isNew, true);

                if (currentValue == null && !propertymeta.DataType.CanAssignedNull())
                {
                    var allowNull = false;

                    if (propertymeta is IUpdatableProperty p)
                    {
                        allowNull = isNew && p.IsIdentity;
                    }

                    if (!allowNull && propertymeta is IPocoProperty property)
                    {
                        allowNull = property.SaveStrategy == SaveStrategy.ReadAfterSave ||
                            (isNew &&
                            (property.SaveStrategy == SaveStrategy.Ignore ||
                            property.Attribute.DefaultValueDefined ||
                            property.Attribute.SqlDefaultValueDefined ||
                            (property.Attribute.DatabaseGeneratedDefined &&
                            property.Attribute.DatabaseGenerated.DatabaseGeneratedOption != DatabaseGeneratedOption.None)));
                    }

                    if (!allowNull)
                    {
                        throw new FormatException(String.Format(Properties.Resource.PROPERTY_NULLVALUE_EXCEPTION, name));
                    }
                }

                if ((isSimple && currentValue != null) ||
                    (!isSimple && entry.ModelState == ModelState.NewModified && currentValue != null))
                {
                    propertyState = PropertyState.Modified;

                    originalValue = propertymeta.DataType.DefaultValue();
                }
                else
                {
                    originalValue = propertyState != null
                        ? this.GetValue(name, originalValue, propertymeta.DataType)
                        : propertymeta.DataType.DefaultValue();
                }
            }

            if (!_importeds.Contains(name))
            {
                _importeds.Add(name);

                entry.SetPropertyEntry(
                    name, currentValue, originalValue, propertyState ?? PropertyState.NotModified);
            }
        }

        protected virtual object GetValue(
            string property, object value, Type dataType, bool nullable = false, bool isCount = false)
        {
            try
            {
                var result = ValueConvert.Convert(value, dataType);

                if (isCount)
                {
                    _sccessed++;
                }

                return result;
            }
            catch (Exception e)
            {
                var msg = $"Invalid data format, name: {property}, value: {value}";

                if (Type.GetTypeCode(dataType) == TypeCode.DateTime)
                {
                    msg = $"Invalid datetime format, name: {property}, value: {value}";
                }

                throw new FormatException(msg, e);
            }
        }

        protected bool FilterColumn(int columnNum, string propertyName)
        {
            var canReadColumn = false;

            switch (_setting.MappingMethod)
            {
                case MappingMethod.Index:
                {
                    canReadColumn = this.CanReadColumn(columnNum, columnNum);
                }
                break;
                case MappingMethod.ColumnIndex:
                {
                    if (!String.IsNullOrWhiteSpace(propertyName) &&
                        _metaColumns != null && _metaColumns.ContainsKey(propertyName))
                    {
                        canReadColumn = this.CanReadColumn(_metaColumns[propertyName].Index, columnNum);
                    }
                }
                break;
                case MappingMethod.Key:
                {
                    canReadColumn = this.CanReadColumn(columnNum, columnNum);
                }
                break;
            }

            return canReadColumn;
        }

        private IPropertyMeta GetPropertyMeta(int nodeIndex, string propertyName)
        {
            switch (_setting.MappingMethod)
            {
                case MappingMethod.Index:
                {
                    return _propertyMetas.FirstOrDefault(x => x.Index == this.GetPropertyIndex(nodeIndex));
                }
                case MappingMethod.ColumnIndex:
                {
                    if (!String.IsNullOrWhiteSpace(propertyName) &&
                        _metaColumns != null && _metaColumns.ContainsKey(propertyName))
                    {
                        return _propertyMetas.FirstOrDefault(
                            x => x.Index == this.GetPropertyIndex(_metaColumns[propertyName].Index));
                    }
                }
                break;
                case MappingMethod.Key:
                {
                    if (_modelProperties.ContainsKey(propertyName))
                    {
                        return _modelProperties[propertyName];
                    }
                }
                break;
            }

            return null;
        }

        private int GetPropertyOffset(int nodeIndex)
        {
            return _setting.StartProperty >= 0
               ? nodeIndex - _setting.StartProperty
               : nodeIndex;
        }

        private int GetPropertyIndex(int nodeIndex)
        {
            if (_setting.DwStartColumn < 0)
            {
                return _importeds.Count;
            }

            var offset = this.GetPropertyOffset(nodeIndex);

            return _setting.DwStartColumn + offset;
        }

        private bool CheckDwColumn(int columnNum)
        {
            if (_modelMeta == null)
            {
                return true;
            }

            var length = _setting.DwStartColumn >= 0
                ? _modelMeta.PropertyCount - _setting.DwStartColumn
                : _modelMeta.PropertyCount;

            return length > this.GetPropertyOffset(columnNum);
        }

        private bool CanReadColumn(int index, int columnNum)
        {
            return columnNum >= _setting.StartProperty && this.CheckDwColumn(columnNum) &&
                (_setting.EndProperty < 0 || columnNum <= _setting.EndProperty);
        }

        protected (object current, PropertyState? state, object original) GetNodeData(INodeReader nodeReader, int nodeIndex)
        {
            object current = null;
            object original = null;
            PropertyState? state = null;

            var isDecimal = this.IsDecimalType(nodeReader, nodeIndex);

            if (isDecimal)
            {
                this.FloatParseByDecimal(nodeReader);
            }

            for (var i = 0; !nodeReader.IsEnd && nodeReader.Read(); i++)
            {
                var currentNode = nodeReader;

                if (currentNode.CurrentNodeType != DataNodeType.Value)
                {
                    currentNode = currentNode.ReadAsNode();

                    if (!currentNode.Read())
                    {
                        continue;
                    }
                }

                switch (i)
                {
                    case 0: current = isDecimal ? currentNode.ValueReader.ToDecimal() : currentNode.ValueReader.Value; break;
                    case 1: state = (PropertyState)currentNode.ValueReader.ToInt32(); break;
                    case 2: original = isDecimal ? currentNode.ValueReader.ToDecimal() : currentNode.ValueReader.Value; break;
                }
            }

            if (isDecimal)
            {
                this.RestoreFloatParseHandling(nodeReader);
            }

            return (current, state, original);
        }

        private void FloatParseByDecimal(INodeReader nodeReader)
        {
            if (_modelMeta != null && nodeReader is JNodeReader reader)
            {
                reader.ParseFloatByDecimal();
            }
        }

        private void RestoreFloatParseHandling(INodeReader nodeReader)
        {
            if (_modelMeta != null && nodeReader is JNodeReader reader)
            {
                reader.ParseFloatByDefault();
            }
        }

        private bool IsDecimalType(INodeReader nodeReader, int nodeIndex)
        {
            var name = String.IsNullOrWhiteSpace(nodeReader.CurrentNodeName) ? nodeReader.Name : nodeReader.CurrentNodeName;

            if (_modelMeta != null && !String.IsNullOrWhiteSpace(name))
            {
                var property = this.GetPropertyMeta(nodeIndex, name);

                if (property != null && (property.DataType == typeof(decimal) || property.DataType == typeof(decimal?)))
                {
                    return true;
                }
            }

            return false;
        }
    }
}
